// 
// Copyright (c) 2006-2009 Ben Motmans
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//
// Author(s):
//    Ben Motmans <ben.motmans@gmail.com>
//

using System;
using System.Text;
using System.Reflection;
using System.Diagnostics;
using System.Collections.Generic;

namespace Anculus.Core
{
	/// <summary>
	/// 
	/// </summary>
	public static class Log
	{
		/// <summary>
		///  
		/// </summary>
		public static event LogEntryEventHandler MessageLogged;

		private static List<ILogBackend> backends;
		private static int minimalLogLevel;

		/// <summary>
		/// Initializes the <see cref="Log"/> class.
		/// </summary>
		static Log ()
		{
			backends = new List<ILogBackend> ();

			MinimalLogLevel = LogLevel.Info;
		}

		/// <summary>
		/// 
		/// </summary>
		public static LogLevel MinimalLogLevel
		{
			get { return (LogLevel)minimalLogLevel; }
			set { minimalLogLevel = (int)value; }
		}
		
		public static bool IgnoreLogSource { get; set; }
		
		#region Configuration
		public static void Configure ()
		{
			//TODO: write to stderr if something fails inside this method
			
			//load backends
			IConfiguration config = OpenLogConfiguration ();
			foreach (IConfigurationSection backendSection in config["init"].Sections) {
				//check if the ILogBackend type is defined
				string typeString = backendSection.GetString ("Type");
				string assemblyString = backendSection.GetString ("Assembly");
				
				if (String.IsNullOrEmpty (typeString))
					continue;
				
				//try to load the type
				Type backendType = null; 
				if (assemblyString == null) {
					backendType = Type.GetType (typeString, false);
				} else {
					Assembly ass = Assembly.Load (assemblyString);
					if (ass != null)
						backendType = ass.GetType (typeString, false);
				}
				
				if (backendType == null)
					continue;
				
				//check if the ctor needs parameters
				object[] ctorParams = null;
				IConfigurationSection paramsSection = backendSection["ctor"];
				if (paramsSection.KeyCount > 0) { //the ctor needs parameters
					ctorParams = new object[paramsSection.KeyCount];
					
					//each key name is the ctor param index
					foreach (string indexStr in paramsSection.Keys) {
						int index = 0;
						if (int.TryParse (indexStr, out index)) {
							if (index < 0 || index >= ctorParams.Length)
								continue;
							
							ctorParams[index] = paramsSection.GetObject (indexStr);
						}
					}
				}
				
				//initialize the backend
				try {
					ILogBackend backend = Activator.CreateInstance (backendType, ctorParams) as ILogBackend;
					if (backend != null)
						AddBackend (backend);
					//each backend is configured when Log.AddBackend is called
				} catch { }
			}
			
			//read the global settings
			IConfigurationSection settingsSection = config["settings"];
			try {
				Log.MinimalLogLevel = (LogLevel)Enum.Parse (typeof (LogLevel), settingsSection.GetString ("MinimalLogLevel", "Info"), true);
				Log.IgnoreLogSource = settingsSection.GetBool ("IgnoreLogSource");
			} catch { }
		}
		
		internal static void ConfigureBackend (ILogBackend backend)
		{
			IConfiguration config = OpenLogConfiguration ();

			string sectionName = backend.GetType ().FullName;
			if (sectionName.StartsWith ("Anculus.")) //ILogBackends defined in anculus support short names
				sectionName = sectionName.Substring (sectionName.LastIndexOf ("."));

			if (config.ContainsSection("backend")) {
				IConfigurationSection section = config["backend"];

				if (section.ContainsSection ("default"))
					backend.LoadSettings (section["default"]);

				if (section.ContainsSection (sectionName))
					backend.LoadSettings (section[sectionName]);

				if (section.ContainsSection ("override"))
					backend.LoadSettings (section["override"]);
			}
		}
		
		private static IConfiguration OpenLogConfiguration ()
		{
			Assembly ass = Assembly.GetEntryAssembly ();
			string configFile = null;
			
			//check if the given assembly has the LogConfiguration attribute set
			object[] attribs = ass.GetCustomAttributes (typeof (LogConfigurationAttribute), true);
			if (attribs.Length > 0) {
				LogConfigurationAttribute lca = (LogConfigurationAttribute)attribs[0];
				configFile = lca.FileName;
			}
			
			//otherwise use the location of the assembly - .ext + .log.config
			string loc = ass.Location;
			int lastdot = loc.LastIndexOf (".");
			if (lastdot > 0)
				configFile = loc.Substring (0, lastdot) + ".log.config";
			
			if (configFile == null)
				return new MemoryConfiguration ();

			//load the config
			XmlConfiguration config = new XmlConfiguration (configFile);
			config.Load ();
			
			return config;
		}
		#endregion
		
		#region Backend management
		/// <summary>
		/// 
		/// </summary>
		/// <param name="backend">
		/// A <see cref="ILogBackend"/>
		/// </param>
		public static void AddBackend (ILogBackend backend)
		{
			if (backend == null)
				throw new ArgumentNullException ("backend");

			lock (backends) {
				if (!backends.Contains (backend)) {
					ConfigureBackend (backend);
					
					backends.Add (backend);
				}
			}
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="backend">
		/// A <see cref="ILogBackend"/>
		/// </param>
		public static void RemoveBackend (ILogBackend backend)
		{
			if (backend == null)
				throw new ArgumentNullException ("backend");

			lock (backends) {
				backends.Remove (backend);
			}
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="type">
		/// A <see cref="Type"/>
		/// </param>
		public static void RemoveBackend (Type type)
		{
			if (type == null)
				throw new ArgumentNullException ("type");
			
			lock (backends) {
				int index = -1;
				for (int i=0; i < backends.Count; i++) {
					if (backends[i].GetType () == type) {
						index = i;
						break;
					}
				}
	
				if (index >= 0) {
					backends.RemoveAt (index);
				}
			}
		}
		#endregion
		
		#region Log methods
		/// <summary>
		/// 
		/// </summary>
		[Conditional ("DEBUG"), Conditional ("TRACE")]
		public static void Trace ()
		{
			if ((int)LogLevel.Trace < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Trace, null, null, null);
		}
		
		/// <summary>
		/// 
		/// </summary>
		[Conditional ("DEBUG"), Conditional ("TRACE")]
		public static void TraceStack ()
		{
			TraceStack (int.MaxValue);
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="stackDepth">
		/// A <see cref="System.Int32"/>
		/// </param>
		[Conditional ("DEBUG"), Conditional ("TRACE")]
		public static void TraceStack (int stackDepth)
		{
			if ((int)LogLevel.Trace < minimalLogLevel)
				return;

			WriteLog (LogLevel.Trace, null, null, null);
			
			StackTrace st = new StackTrace (false);

			if (stackDepth != int.MaxValue)
				++stackDepth; //add 1 since we exclude the stackframe from this method

			if (stackDepth > st.FrameCount)
				stackDepth = st.FrameCount;
			
			lock (backends) {
				for (int i=1; i<stackDepth; i++) { //start from 1 to exclude Log.TraceStack
					string methodSig = GetMethodSignature (st.GetFrame (i).GetMethod (), false);
					
					foreach (ILogBackend backend in backends)
						backend.Write (methodSig);
				}
			}
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="condition">
		/// A <see cref="System.Boolean"/>
		/// </param>
		[Conditional ("DEBUG"), Conditional ("TRACE")]
		public static void TraceIf (bool condition)
		{
			if (!condition || (int)LogLevel.Trace < minimalLogLevel)
				return;

			WriteLog (LogLevel.Trace, null, null, null);
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		[Conditional ("DEBUG"), Conditional ("TRACE")]
		public static void Trace (string message, params object[] args)
		{
			if ((int)LogLevel.Trace < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Trace, message, args, null);
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="condition">
		/// A <see cref="System.Boolean"/>
		/// </param>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		[Conditional ("DEBUG"), Conditional ("TRACE")]
		public static void TraceIf (bool condition, string message, params object[] args)
		{
			if (!condition || (int)LogLevel.Trace < minimalLogLevel)
				return;

			WriteLog (LogLevel.Trace, message, args, null);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		[Conditional ("DEBUG")]
		public static void Debug (string message, params object[] args)
		{
			if ((int)LogLevel.Debug < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Debug, message, args, null);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Info (string message, params object[] args)
		{
			if ((int)LogLevel.Info < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Info, message, args, null);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Warn (string message, params object[] args)
		{
			if ((int)LogLevel.Warning < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Warning, message, args, null);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="ex">
		/// A <see cref="Exception"/>
		/// </param>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Warn (Exception ex, string message, params object[] args)
		{
			if ((int)LogLevel.Warning < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Warning, message, args, ex);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Error (string message, params object[] args)
		{
			if ((int)LogLevel.Error < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Error, message, args, null);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="ex">
		/// A <see cref="Exception"/>
		/// </param>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Error (Exception ex, string message, params object[] args)
		{
			if ((int)LogLevel.Error < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Error, message, args, ex);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Fatal (string message, params object[] args)
		{
			if ((int)LogLevel.Fatal < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Fatal, message, args, null);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <param name="ex">
		/// A <see cref="Exception"/>
		/// </param>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void Fatal (Exception ex, string message, params object[] args)
		{
			if ((int)LogLevel.Fatal < minimalLogLevel)
				return;
			
			WriteLog (LogLevel.Fatal, message, args, ex);
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="arr">
		/// A <see cref="System.Byte[]"/>
		/// </param>
		public static void HexDump (byte[] arr)
		{
			if ((int)LogLevel.Debug < minimalLogLevel)
				return;
			
			lock (backends) {
				try {
					foreach (ILogBackend backend in backends)
						backend.Open ();
					
					StringBuilder result = new StringBuilder (88); // 88 = maximum characters per line
					StringBuilder hexString = new StringBuilder (32 + 16); // 32 = 16 hex values + 16 spaces
					StringBuilder textString = new StringBuilder (16); // 16 text characters
					
					for (int lines = 0; lines < arr.Length; lines += 16) {
						result.Length = 0;
						hexString.Length = 0;
						textString.Length = 0;
						
						for (int i = 0; i < 16; i++) {
							if ((lines + i) >= arr.Length) {
								hexString.Append ("   ");
								textString.Append (" ");
							} else {
								byte b = arr [lines + i];
								hexString.AppendFormat ("{0:X2} ", b);
								textString.Append ((b < 32) ? '.' : (char)b);
							}
						}
						
						result.AppendFormat ("{0:X8}\t", lines);
						result.Append (hexString.ToString ());
						result.Append ("\t");
						result.Append (textString.ToString ());
						result.Append (Environment.NewLine);
						
						foreach (ILogBackend backend in backends)
							backend.Write (result.ToString ());
					}
					
					
				} finally {
					foreach (ILogBackend backend in backends) {
						try {
							if (backend.IsOpen)
								backend.Close ();
						} catch { }
					}
				}
			}
		}
		
		/// <summary>
		/// 
		/// </summary>
		/// <param name="arr">
		/// A <see cref="System.Byte[]"/>
		/// </param>
		/// <param name="message">
		/// A <see cref="System.String"/>
		/// </param>
		/// <param name="args">
		/// A <see cref="System.Object[]"/>
		/// </param>
		public static void HexDump (byte[] arr, string message, params object[] args)
		{
			if ((int)LogLevel.Trace < minimalLogLevel)
				return;

			Log.Debug (message, args);
			HexDump (arr);
		}
		#endregion
		
		#region Internals
		private static void WriteLog (LogLevel level, string message, object[] args, Exception ex)
		{
			//if there is no backend, we log to stdout
			if (backends.Count == 0) {
				lock (backends)
					backends.Add (new ConsoleLogBackend ());
			}
			
			LogEntry entry = new LogEntry ();
			entry.Exception = ex;
			entry.LogLevel = level;
			
			// format the message
			if (message != null) {
				if (args == null || args.Length == 0)
					entry.Message = message;
				else
					entry.Message = String.Format (message, args);
			} else {
				entry.Message = String.Empty;
			}
			
			// timestamp
			entry.TimeStamp = DateTime.Now;
			
			// thread info
			entry.ThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId;
			entry.ThreadName = System.Threading.Thread.CurrentThread.Name;

			//get the logger and source if needed
			if (!IgnoreLogSource) {
				StackTrace st = new StackTrace (true);
				StackFrame sf = st.GetFrame (2);
				MethodBase callingMethod = sf.GetMethod ();
				
				entry.Logger = GetMethodSignature (callingMethod, true);
				entry.Source = sf.GetFileName () + ":" + sf.GetFileLineNumber ();
			}

			//send the message to each logger
			lock (backends) {
				foreach (ILogBackend backend in backends) {
					try {
						backend.Open ();
						backend.WriteEntry (entry);
					} finally {
						backend.Close ();
					}
				}
			}
			
			if (MessageLogged != null)
				MessageLogged (new LogEntryEventArgs (entry));
		}
		#endregion
		
		#region Helper methods		
		private static string GetMethodSignature (MethodBase method, bool shortSig)
		{
			StringBuilder sb = new StringBuilder (128);
			
			if (!shortSig) {
				sb.Append ("\t");
				
				//TODO: return type
				//if (!method.IsConstructor)
				//	sb.Append (GetShortTypeName (method.ReturnType.FullName));
			}
	
			//class.method
			sb.Append (method.DeclaringType.Name);
			sb.Append (".");
			if (method.IsConstructor)
				sb.Append (method.DeclaringType.Name);
			else
				sb.Append (method.Name);
			
			
			if (!shortSig) {
				//generic params
				if (method.IsGenericMethod) {
					sb.Append ("<");

					bool firstGeneric = true;
					foreach (Type genericType in method.GetGenericArguments ()) {
						if (firstGeneric) firstGeneric = false;
						else sb.Append (", ");
						
						sb.Append (GetShortTypeName (genericType.FullName));
					}

					
					sb.Append (">");
				}
				
				
				//method params
				sb.Append ("(");
				
				bool firstParam = true;
				foreach (ParameterInfo param in method.GetParameters ()) {
					if (firstParam) firstParam = false;
					else sb.Append (", ");
					
					if (param.IsOut)
						sb.Append ("out ");
					//TODO: ref

					sb.Append (GetShortTypeName (param.ParameterType.FullName));
				}
				
				sb.Append (")");
			}
			
			return sb.ToString ();
		}
		
		private static string GetShortTypeName (string name)
		{
			string postfix = String.Empty;
			if (name.EndsWith ("]")) { //an array
				int pos = name.IndexOf ("[");
				if (pos >= 0) {
					postfix = name.Substring (pos);
					name = name.Substring (0, pos);
				}
			}
			
			switch (name) {
				case "System.Byte": return "byte" + postfix;
				case "System.Int16": return "short" + postfix;
				case "System.UInt16": return "ushort" + postfix;
				case "System.Int32": return "int" + postfix;
				case "System.UInt32": return "uint" + postfix;
				case "System.Int64": return "long" + postfix;
				case "System.UInt64": return "ulong" + postfix;
				case "System.Boolean": return "bool" + postfix;
				case "System.String": return "string" + postfix;
				case "System.Char": return "char" + postfix;
				case "System.Float": return "float" + postfix;
				case "System.Double": return "double" + postfix;
				case "System.Decimal": return "decimal" + postfix;
				case "System.Object": return "object" + postfix;
				default: return name + postfix;
			}
		}
		#endregion
	}
}
